package com.easyUploader.service.impl;
import com.common.exception.CustomException;
import com.easyUploader.entity.vo.FileChunkVo;
import com.easyUploader.property.LocalFileProperty;
import com.easyUploader.service.IBucketService;
import com.easyUploader.service.IFileChunkService;
import com.easyUploader.service.IFileService;
import com.easyUploader.service.IFileStorageService;
import com.easyUploader.util.FileUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import sun.misc.Cleaner;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.lang.reflect.Method;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.security.AccessController;
import java.security.PrivilegedAction;


/**
 * @author zdh
 */
@Service
@Slf4j
@RequiredArgsConstructor
public class LocalFileServiceImpl implements IFileService {
    private final IFileChunkService fileChunkService;
    private final IFileStorageService fileStorageService;
    private final LocalFileProperty localFileProperty;
    private final IBucketService iBucketService;


    @Transactional
    @Override
    public String uploadFile(FileChunkVo fileChunkVo) {
        //先检查是否可以进行秒传
        if(fileStorageService.checkFileUpLoad(fileChunkVo.getFileIdentifier())!=null||fileChunkVo.checkChunkUpLoad(fileChunkService)){
            log.info("当前分片采用秒传策略");
            return fileChunkVo.getVisitUrl(localFileProperty);
        }
        fileChunkVo.safeCheck(localFileProperty,iBucketService,fileChunkService,fileStorageService);
        log.info(       "当前文件名: {}" +
                        ",文件的相对路径: {}"+
                        ",文件总大小: {}"+
                        ",分片标号: {}" +
                        ",分片大小: {}" +
                        ",当前分片实际承载数据大小: {}" +
                        ",总分片数: {}"+
                        ",当前文件对应md5校验码: {}"+
                        ",当前分片对应md5校验码: {}"
                ,fileChunkVo.getFileName(),fileChunkVo.getDirectoryPath()==null?" ":fileChunkVo.getDirectoryPath(),fileChunkVo.getTotalSize(),fileChunkVo.getChunkNumber(),
                fileChunkVo.getChunkSize(),fileChunkVo.getDataSize(),fileChunkVo.getTotalChunk(),
                fileChunkVo.getFileIdentifier(),fileChunkVo.getChunkIdentifier());

        //文件没有采用重命名方式防止重复发生,如果出现同一个目录下文件名重复,那么抛出异常
        //fileChunkVo.renameFile();
        String wrappedFullFilePath = fileChunkVo.getWrappedFilePath(localFileProperty);

        if(fileChunkVo.getChunkNumber().equals(1)&&FileUtil.repeatFileCheck(wrappedFullFilePath)){
            throw new CustomException(fileChunkVo.getDirectoryPath()+"目录下出现重复文件名"+fileChunkVo.getFileName(),fileChunkVo.getDirectoryPath()+"目录下出现重复文件名"+fileChunkVo.getFileName());
        }

        // 单文件上传
        if (fileChunkVo.getTotalChunk() == 1) {
            return uploadSingleFile(wrappedFullFilePath, fileChunkVo);
        }

        // 分片上传，这里使用 uploadFileByRandomAccessFile 方法，也可以使用 uploadFileByMappedByteBuffer 方法上传
        boolean flag = uploadFileByMappedByteBuffer(wrappedFullFilePath, fileChunkVo);

        if (!flag) {
            throw new CustomException("文件上传失败","文件上传失败");
        }

        //保存分片上传信息
        saveFileTrunk(fileChunkVo, wrappedFullFilePath);

        return fileChunkVo.getVisitUrl(localFileProperty);
    }

    private void saveFileTrunk(FileChunkVo fileChunkVo, String warppedFullFilePath) {
        fileChunkService.saveFileChunk(fileChunkVo, fileChunkVo1 -> {
            fileStorageService.saveLocalStorage(warppedFullFilePath,fileChunkVo1);
        });
    }


    private boolean uploadFileByRandomAccessFile(String resultFileName, FileChunkVo param) {
        try (RandomAccessFile randomAccessFile = new RandomAccessFile(resultFileName, "rw")) {
            // 分片大小必须和前端匹配，否则上传会导致文件损坏
            long chunkSize = param.getChunkSize() == 0L ? localFileProperty.getChunkSize() : param.getChunkSize().longValue();
            // 偏移量
            long offset = chunkSize * (param.getChunkNumber() - 1);
            // 定位到该分片的偏移量
            randomAccessFile.seek(offset);
            // 写入
            randomAccessFile.write(param.getFile().getBytes());
        } catch (IOException e) {
            log.error("文件上传失败：" + e);
            return false;
        }
        return true;
    }

    private boolean uploadFileByMappedByteBuffer(String resultFileName, FileChunkVo param) {
        // 分片上传
        try (RandomAccessFile randomAccessFile = new RandomAccessFile(resultFileName, "rw");
             FileChannel fileChannel = randomAccessFile.getChannel()) {
            // 分片大小必须和前端匹配，否则上传会导致文件损坏
            long chunkSize = param.getChunkSize() == 0L ? localFileProperty.getChunkSize() : param.getChunkSize().longValue();
            // 写入文件
            long offset = chunkSize * (param.getChunkNumber() - 1);
            byte[] fileBytes = param.getFile().getBytes();
            MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, offset, fileBytes.length);
            mappedByteBuffer.put(fileBytes);
            // 释放
            unmap(mappedByteBuffer);
        } catch (IOException e) {
            log.error("文件上传失败：" + e);
            return false;
        }
        return true;
    }

    private String uploadSingleFile(String resultFileName, FileChunkVo fileChunkVo) {
        File saveFile = new File(resultFileName);
        try {
            // 写入
            fileChunkVo.getFile().transferTo(saveFile);
            saveFileTrunk(fileChunkVo,resultFileName);
        } catch (IOException e) {
            log.error("文件上传失败：" + e);
            throw new CustomException("文件上传失败","文件上传失败");
        }
        return fileChunkVo.getVisitUrl(localFileProperty);
    }

    /**
     * 释放 MappedByteBuffer
     * 在 MappedByteBuffer 释放后再对它进行读操作的话就会引发 jvm crash，在并发情况下很容易发生
     * 正在释放时另一个线程正开始读取，于是 crash 就发生了。所以为了系统稳定性释放前一般需要检
     * 查是否还有线程在读或写
     * 来源：https://my.oschina.net/feichexia/blog/212318
     *
     * @param mappedByteBuffer mappedByteBuffer
     */
    public static void unmap(final MappedByteBuffer mappedByteBuffer) {
        try {
            if (mappedByteBuffer == null) {
                return;
            }
            mappedByteBuffer.force();
            AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
                try {
                    Method getCleanerMethod = mappedByteBuffer.getClass()
                            .getMethod("cleaner");
                    getCleanerMethod.setAccessible(true);
                    Cleaner cleaner =
                            (Cleaner) getCleanerMethod
                                    .invoke(mappedByteBuffer, new Object[0]);
                    cleaner.clean();
                } catch (Exception e) {
                    log.error("MappedByteBuffer 释放失败：" + e);
                }
                log.info("clean MappedByteBuffer completed");
                return null;
            });
        } catch (Exception e) {
            log.error("unmap error:" + e);
        }
    }
}